package logica;

import com.herotactics.fuentedatos.FuenteDatos;
import exceptions.PlayerWonException;
import io.ObjectInputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectStreamClass;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import jugador.Jugador;
import mapa.Coordenada;
import mapa.Mapa;
import razas.Raza;
import reflection.JarUtils;
import terrenos.Casilla;
import unidades.Unidad;
import unidades.UnidadHT;

/**
 * Clase que gestiona toda la ejecución de una partida de Hero Tactics
 *
 * @author Ibai
 */
public class ModeloHT extends ModeloEjecucion {

    private UnidadHT unidadActual = null;
    private Collection<Coordenada> rangoMovActual = new ArrayList<Coordenada>();
    private Collection<Coordenada> rangoAtqActual = new ArrayList<Coordenada>();
    public static final int maxPuntosPorEquipo = 1500;

    /**
     * Crea un nuevo modelo y actualiza todos los recursos
     */
    public ModeloHT() {
        refresh();
    }

    /**
     * Crea un nuevo modelo de datos con una fuente de datos y unas razas establecidas, y luego actualiza los mapas
     * 
     * @param fd La fuente de datos
     * @param razas Las razas
     */
    public ModeloHT(FuenteDatos fd, HashMap<String, Raza> razas) {
        setFuenteDatos(fd);
        Raza.setRazas(razas);
        refreshMaps();
    }

    /**
     * Método que realiza sobre la lógica la acción que se determine
     * en una determinada coordenada
     *
     * @param crdnd Coordenada sobre la que realizar la acción
     * @param accion Acción a realizar. Se deberán pasar las siguientes acciones
     * dependiendo de lo que se quiera hacer:
     * <ul><li>AccionRangoAtaque: para mostrar el rango de ataque de la unidad en la coordenada</li>
     * <li>AccionAtacar: para atacar con la unidad seleccionada a la unidad en la coordenada. 
     * Si la unidad defensora muere, su coordenada pasara a ser null</li>
     * <li>AccionRangoMovimiento: para mostrar el rango de movimiento de la unidad en la coordenada</li>
     * <li>AccionMovimiento: para mover la unidad seleccionada a la coordenada</li>
     * <li>AccionRegenerar: para regenerar la unidad seleccionada</li>
     * <li>AccionCancelarMovimiento: cancela el movimiento de la unidad seleccionada</li>
     * <li>AccionCancelarAtaque: cancela el ataque de la unidad seleccionada</li>
     * <li>AcccionFinTurno: Finaliza el turno y cambia de jugador</li></ul>
     * @return Si se está realizando un simple clic en pantalla para escoger
     * una casilla en la que hay o no hay una unidad, es decir, se pasa null
     * como accion, se devolverá la unidad o null. Si la devolución no es null,
     * se deberá pintar el menú de esa unidad.<br/>
     * Si se trata de otra acción, se realizará lo siguiente dependiendo de la acción:
     * <ul><li>AccionRangoAtaque: devuelve una Collection<Coordenada> que contendrá las casillas
     * a las que puede atacar la unidad
     * <li>AccionAtacar: realizará el ataque y actualizará los parmámetros, y devuelve una
     * List<Coordenada> que contiene la posición del atacante y del defensor. Se puede acceder
     * a los datos con los atributos estáticos de la clase</li>
     * <li>AccionRangoMovimiento: Devolverá una Collection<Coordenada> que contendrá
     * las casillas a las que se puede mover la unidad y que hay que dibujar</li>
     * <li>AccionMovimiento: Devuelve una List<Coordenada> que contiene la
     * posición inicial y la final. Se puede acceder a los datos con los atributos
     * estáticos de la clase</li>
     * <li>AccionRegenerar: Realizará la acción de regenerar y actualizará el estado
     * de la unidad. Devuelve la casilla en la que está la unidad y que hay que actualizar</li>
     * <li>AccionCancelarMovimiento: Cancela el movimiento de la unidad y devuelve null</li>
     * <li>AccionCancelarAtaque: Cancela el ataque de la unidad y devuelve null</li>
     * <li>AccionFinTurno: Finaliza el turno y devuelve null</li></ul>
     * Como norma general se deberá pintar la opción devuelta, es decir, el menú de la
     * unidad si se devuelve una unidad, actualizar la casilla si se devuelve una coordenada
     * o nada si se devuelve null, y después actualizar la casilla sobre la que se
     * ha hecho la acción.
     * @throws NullPointerException Si la coordenada es null
     */
    public Object accionEnCasilla(Coordenada crdnd, Accion accion) throws PlayerWonException {
        if (crdnd == null) {
            throw new NullPointerException("La coordenada no puede ser nula");
        }
        /* Si la acción es nula, es simplemente un clic en pantalla que puede significar:
         * Seleccionar una unidad en una casilla (que puede actuar o no)
         * Seleccionar una casilla vacía */
        if (accion == null) {
            rangoAtqActual.clear();
            rangoMovActual.clear();
            unidadActual = (UnidadHT) getPartida().getMapa().getContenidoCasilla(crdnd);
            if (!getPartida().getTurno().getJugadorActivo().getEquipo().contains(unidadActual)
                    || (unidadActual != null && !unidadActual.puedeActuar())) {
                //Si la unidad no está en el equipo del jugador activo,
                //o si la casilla no está vacía pero la unidad no puede actuar,
                //se establece la unidad como null ya que no es válida
                unidadActual = null;
            }
            return unidadActual;
        } else {
            if (!(accion instanceof AccionFinTurno) && unidadActual == null) {
                throw new NullPointerException("No hay seleccionada ninguna unidad");
            }
            Object retorno = accion.ejecutarAccion(this, unidadActual, rangoMovActual, rangoAtqActual, crdnd);

            checkWinner();
            return retorno;
        }
    }

    /**
     * Comprueba si ha ganado algún jugador.
     * 
     * @throws PlayerWonException Se lanzará si algún jugador ha ganado
     */
    private void checkWinner() throws PlayerWonException {
        Jugador a = getPartida().getJugadorA();
        Jugador b = getPartida().getJugadorB();
        if (a.getEquipo().isEmpty()) {
            throw new PlayerWonException(b);
        }
        if (b.getEquipo().isEmpty()) {
            throw new PlayerWonException(a);
        }
    }

    /**
     * Actualiza los puntos de accion y, si es necesario, finaliza el turno
     */
    public void actualizarTurno() {
        int puntos = getPartida().getTurno().getPuntosAccion() - 1;
        getPartida().getTurno().setPuntosAccion(puntos);
        if (puntos <= 0) {
            finTurno();
        }
    }

    /**
     * Finaliza el turno y cambia de jugador
     */
    public void finTurno() {
        cambiaTurno();
        unidadActual = null;
        rangoAtqActual.clear();
        rangoMovActual.clear();
    }

    /**
     * Guarda la partida actual
     * 
     * @throws Exception Si ocurre algún error con la fuente de datos
     */
    public void guardarPartida() throws Exception {
        FuenteDatos fuenteDatos = getFuenteDatos();
        try {
            fuenteDatos.init();
            fuenteDatos.insertJugador(getPartida().getJugadorA());
            fuenteDatos.insertJugador(getPartida().getJugadorB());
            fuenteDatos.insertPartida(getPartida());
        } finally {
            if (fuenteDatos != null) {
                fuenteDatos.end();
            }
        }
    }

    /**
     * Carga la partida de una fecha determinada
     * 
     * @param fecha El tiempo en milisegundos de la partida
     * @return La partida de esa fecha
     * @throws Exception Si ocurre algún error con la fuente de datos
     */
    public Partida cargarPartida(long fecha) throws Exception {
        FuenteDatos fuenteDatos = getFuenteDatos();
        try {
            fuenteDatos.init();
            Collection<Partida> partidas = fuenteDatos.getAllPartida();
            Partida partida = null;
            for (Partida p : partidas) {
                if (p.getFecha() == fecha) {
                    partida = p;
                    break;
                }
            }
            fuenteDatos.end();
            if (partida == null) {
                throw new IllegalArgumentException("No existe ninguna partida con fecha " + fecha);
            }
            return partida;
        } finally {
            if (fuenteDatos != null) {
                fuenteDatos.end();
            }
        }
    }

    /**
     * Carga todas las partidas de la fuente de datos actual
     * 
     * @return Collection<Partida> que contiene todas las partidas albergadas en la fuente de datos
     * @throws Exception 
     */
    public Collection<Partida> cargarPartidas() throws Exception {
        FuenteDatos fuenteDatos = getFuenteDatos();
        try {
            fuenteDatos.init();
            Collection<Partida> partidas = fuenteDatos.getAllPartida();
            return partidas;
        } finally {
            if (fuenteDatos != null) {
                fuenteDatos.end();
            }
        }
    }

    /**
     * Carga todos los mapas de la carpeta correspondiente
     * 
     * @param casillas Collection<Casilla> que debe contener las distintas casillas utilizadas en los mapas
     * @return HashMap<String, Mapa> con todos los mapas, cuya clave es su nombre
     */
    private HashMap<String, Mapa> loadMapas(Collection<Casilla> casillas) {
        HashMap<String, Mapa> mapas = new HashMap<String, Mapa>();
        File carpeta = new File(getPathAProyecto() + "/Modulos/HeroTactics/Mapas");
        File[] files = carpeta.listFiles(new FileFilter() {

            public boolean accept(File pathname) {
                return pathname.getName().endsWith(".map") && !pathname.isDirectory();
            }
        });
        if (files != null) {
            for (File f : files) {
                java.io.ObjectInputStream ois = null;
                try {
                    FileInputStream fis = new FileInputStream(f);
                    if (casillas != null) {
                        ois = new ObjectInputStream(fis);
                        for (Casilla c : casillas) {
                            ((ObjectInputStream) ois).resolveClass(ObjectStreamClass.lookup(c.getClass()));
                        }
                    } else {
                        ois = new java.io.ObjectInputStream(fis);
                    }
                    mapas.put(f.getName().substring(0, f.getName().length() - 4), (Mapa) ois.readObject());
                    fis.close();
                    ois.close();
                } catch (ClassNotFoundException ex) {
                    ex.printStackTrace();
                } catch (IOException ex) {
                    ex.printStackTrace();
                } finally {
                    try {
                        ois.close();
                    } catch (IOException ex) {
                        ex.printStackTrace();
                    }
                }
            }
        }
        return mapas;
    }

    /**
     * Carga todas las razas existentes
     * 
     * @return HashMap<String, Raza> con todas las razas identificadas por clave con su nombre
     */
    private HashMap<String, Raza> loadRazas() {
        File carpeta = new File(getPathAProyecto() + "/Modulos/HeroTactics/Unidades");
        File[] files = carpeta.listFiles(new FileFilter() {

            public boolean accept(File pathname) {
                return pathname.isDirectory();
            }
        });
        HashMap<String, Raza> razas = new HashMap<String, Raza>(files.length);
        for (File f : files) {
            try {
                Collection<UnidadHT> uds = JarUtils.getObjectsAtDirectory(f.getPath(), ".jar", "unidades");
                Collection<UnidadHT> lideres = JarUtils.getObjectsAtDirectory(f.getPath() + "/Lideres", ".jar", "unidades");
                Raza r = new Raza(f.getName());
                r.setLideres(lideres);
                r.setUnidades(uds);
                razas.put(f.getName(), r);
            } catch (IOException ex) {
                ex.printStackTrace();
            } catch (InstantiationException ex) {
                ex.printStackTrace();
            } catch (IllegalAccessException ex) {
                ex.printStackTrace();
            } catch (ClassNotFoundException ex) {
                ex.printStackTrace();
            }
        }
        return razas;
    }

    /**
     * Carga todas las fuentes de datos en la carpeta correspondiente al modulo
     * 
     * @return Collection<FuenteDatos> con todas las fuentes de datos
     */
    private Collection<FuenteDatos> loadFuentesDatos() {
        Collection<FuenteDatos> fuentesDatos = new ArrayList<FuenteDatos>();
        try {
            Collection<FuenteDatos> fd = JarUtils.getObjectsAtDirectory(getPathAProyecto() + "/Modulos/HeroTactics/FuenteDatos", ".jar", "com.herotactics.fuentedatos");
            fuentesDatos = fd;
        } catch (IOException ex) {
            ex.printStackTrace();
        } catch (InstantiationException ex) {
            ex.printStackTrace();
        } catch (IllegalAccessException ex) {
            ex.printStackTrace();
        } catch (ClassNotFoundException ex) {
            ex.printStackTrace();
        }
        return fuentesDatos;
    }

    /**
     * Carga los nombres de todos los jugadores en la fuente de datos actual
     * 
     * @return Los nombres de todos los jugadores
     * @throws Exception Si ocurre algún error con la fuente de datos
     */
    public Collection<String> loadJugadores() throws Exception {
        FuenteDatos fuenteDatos = getFuenteDatos();
        try {
            fuenteDatos.init();
            Collection<String> allJugadores = fuenteDatos.getAllJugadores();
            return allJugadores;
        } finally {
            if (fuenteDatos != null) {
                fuenteDatos.end();
            }
        }
    }

    /**
     * Coloca en el mapa las unidades de los equipos
     *
     * @param team1 Collection que contiene al equipo 1 con las unidades cuyas coordenadas
     * representan lo siguente: la que más arriba a la izquierda esté es la (0, 0) y la de 
     * más abajo a la derecha la (1, 5)
     * @param team2 Collection que contiene al equipo 1 con las unidades cuyas coordenadas
     * representan lo siguente: la que más arriba a la izquierda esté es la (0, 0) y la de 
     * más abajo a la derecha la (1, 5)
     */
    private void colocarEquipos(Collection<Unidad> team1, Collection<Unidad> team2) {
        Coordenada max = getPartida().getMapa().getCoordenadaMaxima();
        Unidad[][] uds = new Unidad[max.getX() + 1][max.getY() + 1];
        for (Unidad u : team1) {
            Coordenada c = new Coordenada(u.getPosicion().getX(), u.getPosicion().getY() + 2);
            u.setPosicion(c);
            uds[c.getX()][c.getY()] = u;
        }
        for (Unidad u : team2) {
            Coordenada c = new Coordenada(u.getPosicion().getX() + (max.getX() - 1), u.getPosicion().getY() + 3);
            u.setPosicion(c);
            uds[c.getX()][c.getY()] = u;
        }
        getPartida().getMapa().setUnidades(uds);
        getPartida().setPosicionado();
    }

    /**
     * Establece la partida como la actual para este modelo
     * 
     * @param p La partida
     */
    @Override
    public void setPartida(Partida p) {
        super.setPartida(p);
        if (!p.getPosicionado()) {
            colocarEquipos(p.getJugadorA().getEquipo().getUnidades(), p.getJugadorB().getEquipo().getUnidades());
        }
    }

    /**
     * Actualiza todos los mapas de la carpeta correspondiente
     */
    private void refreshMaps() {
        HashMap<String, Mapa> mapas = loadMapas(null);
        Mapa.setMapas(mapas);
    }

    /**
     * Actualiza todos los mapas de la carpeta correspondiente
     * 
     * @param casillas Las casillas que pueden tener los mapas
     */
    private void refreshMaps(Collection<Casilla> casillas) {
        HashMap<String, Mapa> mapas = loadMapas(casillas);
        Mapa.setMapas(mapas);
    }

    /**
     * Actualiza todos los recursos del modelo
     */
    public void refresh() {
        HashMap<String, Raza> razas = loadRazas();
        Raza.setRazas(razas);
        Collection<Casilla> casillas = null;
        try {
            casillas = JarUtils.getObjectsAtDirectory(getPathAProyecto() + "/Modulos/HeroTactics/Terrenos", ".jar", "terrenos");
        } catch (IOException ex) {
            ex.printStackTrace();
        } catch (InstantiationException ex) {
            ex.printStackTrace();
        } catch (IllegalAccessException ex) {
            ex.printStackTrace();
        } catch (ClassNotFoundException ex) {
            ex.printStackTrace();
        }
        refreshMaps(casillas);
        Collection<FuenteDatos> fuentesDatos = loadFuentesDatos();
        setFuentesDatos(fuentesDatos);
        if (fuentesDatos.size() == 1) {
            setFuenteDatos(fuentesDatos.iterator().next());
        }
    }
}
